From 0ee1e6a7ba82f476717ce29d10d959b478441a73 Mon Sep 17 00:00:00 2001 From: "tw275@labyrinth.cl.cam.ac.uk" Date: Fri, 23 Jul 2004 16:19:16 +0000 Subject: [PATCH] bitkeeper revision 1.1108.13.1 (41013a844lXqlHgR5n6-9AZ5s3WCsQ) Added general support for actions Slightly generalized and refactored code for modules / tabs --- .rootkeys | 3 ++ tools/python/xen/sv/DomInfo.py | 22 +++++++++- tools/python/xen/sv/GenTabbed.py | 63 ++++++++++++++++++++++++---- tools/python/xen/sv/HTMLBase.py | 37 ++++++++++++++++ tools/python/xen/sv/Main.py | 40 ++++++++++++------ tools/python/xen/sv/NodeInfo.py | 35 +++++++++------- tools/python/xen/sv/util.py | 16 +++++-- tools/python/xen/xend/XendClient.py | 8 ++++ tools/sv/Makefile | 4 ++ tools/sv/images/reboot.png | Bin 0 -> 3132 bytes tools/sv/images/shutdown.png | Bin 0 -> 2901 bytes tools/sv/inc/script.js | 15 +++++++ tools/sv/inc/style.css | 4 +- 13 files changed, 207 insertions(+), 40 deletions(-) create mode 100755 tools/sv/images/reboot.png create mode 100755 tools/sv/images/shutdown.png create mode 100755 tools/sv/inc/script.js diff --git a/.rootkeys b/.rootkeys index 424d963f29..5ea44cb274 100644 --- a/.rootkeys +++ b/.rootkeys @@ -390,12 +390,15 @@ 40fcefb38OTgsUKHBpwshLLIsiIaCA tools/sv/images/middle-no-highlight.jpg 40fcefb32SPtrw36c4S6YGFlLvkKuw tools/sv/images/orb_01.jpg 40fcefb3Ok5qkX3iM7ZEPVkRInrUpg tools/sv/images/orb_02.jpg +41013a82ILk71xLqWFH5ZO5VmOIvBw tools/sv/images/reboot.png 40fcefb3JnT5XeKTuVF4yUMGOtuNZg tools/sv/images/right-end-highlight.jpg 40fcefb3-DuYOS7noo2W7b_0p7TOUg tools/sv/images/right-end-no-highlight.jpg 40fcefb3qNbAZR5FYGPAZ9sFPVMTDA tools/sv/images/seperator-left-highlight.jpg 40fcefb3dgsa24WLk_BJeYQHrDLuOg tools/sv/images/seperator-right-highlight.jpg 40fcefb3FtiX4Pd2kT8wDlp8u8xRhQ tools/sv/images/seperator.jpg +41013a82sUdUqBv8EoAUJii3gsZ-4g tools/sv/images/shutdown.png 40fcefb3yMSrZvApO9ToIi-iQwnchA tools/sv/images/xen.png +41013a83z27rKvWIxAfUBMVZ1eDCDg tools/sv/inc/script.js 40fcefb3zGC9XNBkSwTEobCoq8YClA tools/sv/inc/style.css 403a3edbrr8RE34gkbR40zep98SXbg tools/xentrace/Makefile 40a107afN60pFdURgBv9KwEzgRl5mQ tools/xentrace/formats diff --git a/tools/python/xen/sv/DomInfo.py b/tools/python/xen/sv/DomInfo.py index 42abdee799..a718871fd5 100755 --- a/tools/python/xen/sv/DomInfo.py +++ b/tools/python/xen/sv/DomInfo.py @@ -14,7 +14,7 @@ class DomInfo( GenTabbed ): def tabUrlWriter( tab ): return urlWriter( "mod=info&dom=%s%s" % ( self.dom, tab ) ) - GenTabbed.__init__( self, tabUrlWriter, [ 'General', 'SXP', 'Devices' ], [ DomGenTab, DomSXPTab, NullTab ] ) + GenTabbed.__init__( self, "Domain Info", tabUrlWriter, [ 'General', 'SXP', 'Devices' ], [ DomGeneralTab, DomSXPTab, NullTab ] ) def write_BODY( self, request ): dom = request.args.get('dom') @@ -27,6 +27,10 @@ class DomInfo( GenTabbed ): GenTabbed.write_BODY( self, request ) +class DomGeneralTab( CompositeTab ): + def __init__( self ): + CompositeTab.__init__( self, [ DomGenTab, DomActionTab ] ) + class DomGenTab( GeneralTab ): def __init__( self ): @@ -41,7 +45,7 @@ class DomGenTab( GeneralTab ): titles[ 'Total CPU' ] = ( 'cpu_time', smallTimeFormatter ) titles[ 'Up Time' ] = ( 'up_time', bigTimeFormatter ) - GeneralTab.__init__( self, "General Domain Info", {}, titles ) + GeneralTab.__init__( self, {}, titles ) def write_BODY( self, request ): @@ -79,4 +83,18 @@ class DomSXPTab( PreTab ): PreTab.write_BODY( self, request ) +class DomActionTab( ActionTab ): + + def __init__( self ): + ActionTab.__init__( self, { "shutdown" : ( "Shutdown the Domain", "shutdown.png" ), + "reboot" : ( "Reboot the Domain", "reboot.png" ) } ) + + def op_shutdown( self, request ): + print ">DomShutDown" + #server.xend_node_shutdown() + + def op_reboot( self, request ): + print ">DomReboot" + #server.xend_node_reboot() + diff --git a/tools/python/xen/sv/GenTabbed.py b/tools/python/xen/sv/GenTabbed.py index bfbe5b04cf..1d5bd2d22a 100755 --- a/tools/python/xen/sv/GenTabbed.py +++ b/tools/python/xen/sv/GenTabbed.py @@ -5,12 +5,13 @@ from xen.sv.TabView import TabView class GenTabbed( HTMLBase ): - def __init__( self, urlWriter, tabStrings, tabObjects ): + def __init__( self, title, urlWriter, tabStrings, tabObjects ): HTMLBase.__init__(self) self.tab = 0; self.tabStrings = tabStrings self.tabObjects = tabObjects self.urlWriter = urlWriter + self.title = title def write_BODY( self, request, urlWriter = None ): tab = request.args.get('tab') @@ -23,19 +24,34 @@ class GenTabbed( HTMLBase ): request.write( "" ) request.write( "
" ) + request.write( "

%s

" % self.title ) + TabView( self.tab, self.tabStrings, self.urlWriter ).write_BODY( request ) request.write( "
" ) - render_tab = self.tabObjects[ self.tab ]() + render_tab = self.tabObjects[ self.tab ] if render_tab is None: request.write( "

Bad Tab

" ) self.finish_BODY( request ) else: - render_tab.write_BODY( request ) + render_tab().write_BODY( request ) request.write( "
" ) + + def perform( self, request ): + tab = request.args.get('tab') + + if tab is None or len( tab) != 1: + self.tab = 0 + else: + self.tab = int( tab[0] ) + + op_tab = self.tabObjects[ self.tab ] + + if op_tab: + op_tab().perform( request ) class PreTab( HTMLBase ): @@ -45,7 +61,7 @@ class PreTab( HTMLBase ): def write_BODY( self, request ): - request.write( "
" )
+        request.write( "
" )
         
         request.write( self.source )
         
@@ -53,16 +69,13 @@ class PreTab( HTMLBase ):
 
 class GeneralTab( HTMLBase ):
                         
-    def __init__( self, title, dict, titles ):
+    def __init__( self, dict, titles ):
         HTMLBase.__init__( self )
-        self.title = title
         self.dict = dict
         self.titles = titles
                         
     def write_BODY( self, request ): 
         
-        request.write( "

%s

" % self.title ) - request.write( "" ) def writeAttr( niceName, attr, formatter=None ): @@ -90,4 +103,38 @@ class NullTab( HTMLBase ): def write_BODY( self, request ): request.write( "

%s

" % self.title ) +class ActionTab( HTMLBase ): + + def __init__( self, actions ): + self.actions = actions + HTMLBase.__init__( self ) + + def write_BODY( self, request ): + request.write("

") + + for ( command, ( text, image ) ) in self.actions.items(): + request.write("") + request.write("  ") + + request.write("

 

") + request.write("

") + +class CompositeTab( HTMLBase ): + + def __init__( self, tabs ): + HTMLBase.__init__( self ) + self.tabs = tabs + + def write_BODY( self, request ): + for tab in self.tabs: + request.write( "
" ) + tab().write_BODY( request ) + + def perform( self, request ): + for tab in self.tabs: + tab().perform( request ) + + + diff --git a/tools/python/xen/sv/HTMLBase.py b/tools/python/xen/sv/HTMLBase.py index 4479d7e455..7c500e8a2b 100755 --- a/tools/python/xen/sv/HTMLBase.py +++ b/tools/python/xen/sv/HTMLBase.py @@ -7,6 +7,10 @@ class HTMLBase( Resource ): def __init__( self ): Resource.__init__(self) + def render_POST( self, request ): + self.perform( request ) + self.render_GET( request ) + def render_GET( self, request ): self.write_TOP( request ) self.write_BODY( request ) @@ -19,7 +23,40 @@ class HTMLBase( Resource ): def write_TOP( self, request ): request.write( 'Xen' ) + request.write( '' ) request.write( '' ) def write_BOTTOM( self, request ): + request.write('' % request.uri) + request.write('') + request.write('') request.write( "" ) + + def get_op_method(self, op): + """Get the method for an operation. + For operation 'foo' looks for 'op_foo'. + + op operation name + returns method or None + """ + op_method_name = 'op_' + op + return getattr(self, op_method_name, None) + + def perform(self, req): + """General operation handler for posted operations. + For operation 'foo' looks for a method op_foo and calls + it with op_foo(req). Replies with code 500 if op_foo + is not found. + + The method must return a list when req.use_sxp is true + and an HTML string otherwise (or list). + Methods may also return a Deferred (for incomplete processing). + + req request + """ + op = req.args.get('op') + if not op is None and len(op) == 1: + op = op[0] + op_method = self.get_op_method(op) + if op_method: + op_method( req ) diff --git a/tools/python/xen/sv/Main.py b/tools/python/xen/sv/Main.py index 6b9bbfae05..50712c7710 100755 --- a/tools/python/xen/sv/Main.py +++ b/tools/python/xen/sv/Main.py @@ -1,14 +1,31 @@ from xen.sv.HTMLBase import HTMLBase -from xen.sv import DomList, NodeInfo, DomInfo +from xen.sv.DomList import DomList +from xen.sv.NodeInfo import NodeInfo +from xen.sv.DomInfo import DomInfo class Main( HTMLBase ): isLeaf = True - def __init__( self ): + def __init__( self, urlWriter = None ): + self.modules = { "node": ( "Node details", NodeInfo ), + "list": ( "Domain summary", DomList ), + "info": ( "Domain info", DomInfo ) } HTMLBase.__init__(self) def render_POST( self, request ): + + #decide what module post'd the action + + mod = request.args.get('mod') + + if not mod is None and len(mod) == 1: + modTup = self.modules[ mod[0] ] + #check module exists + if modTup: + (modName, module) = modTup + module( self.mainUrlWriter ).perform( request ) + return self.render_GET( request ) def mainUrlWriter( self, s ): @@ -25,10 +42,10 @@ class Main( HTMLBase ): request.write( " " ) request.write( " " ) request.write( "
" ) - request.write( "

Node details

" ) - request.write( "

Domains summary

" ) + for (modName, (modTitle, module)) in self.modules.items(): + request.write( "

%s

" % (modName, modTitle)) - DomList.DomList( self.mainUrlWriter ).write_BODY( request, True, False ) + DomList( self.mainUrlWriter ).write_BODY( request, True, False ) request.write( "
" ) @@ -44,14 +61,13 @@ class Main( HTMLBase ): if mod is None or len(mod) != 1: request.write( '

Please select a module

' ) - elif mod[0] == 'info': - DomInfo.DomInfo( self.mainUrlWriter ).write_BODY( request ) - elif mod[0] == 'list': - DomList.DomList( self.mainUrlWriter ).write_BODY( request ) - elif mod[0] == 'node': - NodeInfo.NodeInfo( self.mainUrlWriter ).write_BODY( request ) else: - request.write( '

Invalid module. Please select another

' ) + modTup = self.modules[ mod[0] ] + if modTup: + (modName, module) = modTup + module( self.mainUrlWriter ).write_BODY( request ) + else: + request.write( '

Invalid module. Please select another

' ) request.write( " " ) request.write( " " ) diff --git a/tools/python/xen/sv/NodeInfo.py b/tools/python/xen/sv/NodeInfo.py index 68d050b3bb..f13781bf1a 100755 --- a/tools/python/xen/sv/NodeInfo.py +++ b/tools/python/xen/sv/NodeInfo.py @@ -10,24 +10,18 @@ class NodeInfo( GenTabbed ): def newUrlWriter( url ): return urlWriter( "mod=node%s" % url ) - GenTabbed.__init__( self, newUrlWriter, [ 'General', 'Dmesg' ], [ NodeGenTab, NodeDmesgTab ] ) + GenTabbed.__init__( self, "Node Details", newUrlWriter, [ 'General', 'Dmesg', ], [ NodeGeneralTab, NodeDmesgTab ] ) -class NodeGenTab( PreTab ): +class NodeGeneralTab( CompositeTab ): def __init__( self ): - text = sxp2string( server.xend_node() ) - PreTab.__init__( self, text ) - -class NodeGeneralTab( GeneralTab ): + CompositeTab.__init__( self, [ NodeInfoTab, NodeActionTab ] ) + +class NodeInfoTab( GeneralTab ): def __init__( self ): - nodeInfo = server.xend_node() - - dictNodeInfo = {} - - for l in nodeInfo: - dictNodeInfo[ l[0] ] = l[1] - + nodeInfo = sxp2hash( server.xend_node() ) + dictTitles = {} dictTitles[ 'System' ] = 'system' dictTitles[ 'Hostname' ] = 'host' @@ -40,11 +34,24 @@ class NodeGeneralTab( GeneralTab ): dictTitles[ 'Memory' ] = ( 'memory', memoryFormatter ) dictTitles[ 'Free Memory' ] = ( 'free_memory', memoryFormatter ) - GeneralTab.__init__( self, title="General Node Info", dict=dictNodeInfo, titles=dictTitles ) + GeneralTab.__init__( self, dict=nodeInfo, titles=dictTitles ) class NodeDmesgTab( PreTab ): def __init__( self ): dmesg = server.xend_node_dmesg() PreTab.__init__( self, dmesg[ 1 ] ) + +class NodeActionTab( ActionTab ): + + def __init__( self ): + ActionTab.__init__( self, { "shutdown" : ( "Shutdown the Node", "shutdown.png" ), + "reboot" : ( "Reboot the Node", "reboot.png" ) } ) + + def op_shutdown( self, request ): + print ">NodeShutDown" + #server.xend_node_shutdown() + def op_reboot( self, request ): + print ">NodeReboot" + #server.xend_node_reboot() diff --git a/tools/python/xen/sv/util.py b/tools/python/xen/sv/util.py index 47d3519011..64503bad29 100755 --- a/tools/python/xen/sv/util.py +++ b/tools/python/xen/sv/util.py @@ -17,8 +17,13 @@ def getDomInfoHash( domain ): d['start_time'] = float( sxp.child_value( domInfo, 'start_time' ) ) return d -def sxp2hash( sxp ): - pass +def sxp2hash( s ): + sxphash = {} + + for child in sxp.children( s ): + sxphash[ child[0] ] = child[1] + + return sxphash def sxp2string( sxp ): class tmp: @@ -58,7 +63,12 @@ def stateFormatter( state ): return state def memoryFormatter( mem ): - return "%7dMb" % mem + mem = int( mem ) + if mem >= 1024: + mem = float( mem ) / 1024 + return "%3.2fGb" % mem + else: + return "%7dMb" % mem def cpuFormatter( mhz ): if mhz > 1000: diff --git a/tools/python/xen/xend/XendClient.py b/tools/python/xen/xend/XendClient.py index 2d08f36056..b0a9c8beca 100644 --- a/tools/python/xen/xend/XendClient.py +++ b/tools/python/xen/xend/XendClient.py @@ -366,6 +366,14 @@ class Xend: def xend_node(self): return self.xendGet(self.nodeurl()) + def xend_node_shutdown(self): + return self.xendPost(self.nodeurl(), + {'op' : 'shutdown'}) + + def xend_node_restart(self): + return self.xendPost(self.nodeurl(), + {'op' : 'reboot'}) + def xend_node_dmesg(self): return self.xendGet(self.dmesgurl()) diff --git a/tools/sv/Makefile b/tools/sv/Makefile index 0321a566f3..db432d18be 100755 --- a/tools/sv/Makefile +++ b/tools/sv/Makefile @@ -28,12 +28,16 @@ install: install -m0644 images/seperator.jpg $(sv_insdir)/images install -m0644 images/seperator-left-highlight.jpg $(sv_insdir)/images install -m0644 images/seperator-right-highlight.jpg $(sv_insdir)/images + + install -m0644 images/shutdown.png $(sv_insdir)/images + install -m0644 images/reboot.png $(sv_insdir)/images # make include folder mkdir -p $(sv_insdir)/inc # copy stylesheet install -m0644 inc/style.css $(sv_insdir)/inc + install -m0644 inc/script.js $(sv_insdir)/inc clean: diff --git a/tools/sv/images/reboot.png b/tools/sv/images/reboot.png new file mode 100755 index 0000000000000000000000000000000000000000..358e6deb8fc74e1a89d5f615ffaf4d264e8c89fc GIT binary patch literal 3132 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 feXAVM7;V6O*9ZG8